package com.tavio.tbs.unit;

import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

import org.andengine.entity.sprite.Sprite;

import com.tavio.tbs.scene.SceneManager;
import com.tavio.tbs.structure.Direction;
import com.tavio.tbs.structure.Position;
import com.tavio.tbs.terrain.TerrainType;
import com.tavio.tbs.world.WorldMap;

public abstract class Unit {
	
	private class StackElement {
		
		/* This class represents an element of the stack used in the backtrack 
		 * algorithm of method generatePossibleMovements() */
		
		private Position position;
		
		private Direction lastDirection;
		
		private int stepsSpent;
		
		public StackElement(Position position, Direction direction, int stepsSpent) {
			this.position = position;
			this.lastDirection = direction;
			this.stepsSpent = stepsSpent;
		}

		public Position getPosition() {
			return position;
		}

		public Direction getLastDirection() {
			return lastDirection;
		}
		
		public void setLastDirection(Direction direction) {
			this.lastDirection = direction;
		}

		public int getStepsSpent() {
			return stepsSpent;
		}

	}
	
	
	protected Position position;
	
	protected int stepsAvailable;
	
	protected Sprite sprite;
	
	protected Set<Position> possibleMovements;
	
	protected Map <Position, Deque<Direction>> historyMap;

	protected abstract boolean isPositionValid (Position position);
	
	protected abstract int calculateCostForTerrain (int stepsRemaining, Position terrainPosition, TerrainType terrain);
	
	public void generatePossibleMovements()
	{
		/* Simple backtrack method to generate the unit's possible movement squares.
		 * The method chooses a direction to go (first WEST, then NORTH and so on until SOUTH) and only stops
		 * going on that direction when the unit has run out of steps. In this case, the method goes back to 
		 * the previous position and chooses the next direction. The method ends when we are back to the initial
		 * position and there's no more new directions to try. 
		 * The algorithm will test all the paths to a tile and choose the shortest one. This path will also be
		 * saved in a history. 
		 * 
		 * Note: diagonals cost 2 movement units. For instance: going northeast is considered as first going east
		 * and then going north.*/
		
		StackElement currElement;
		int newX = 0, newY = 0, cost = 0;
		Direction newDirection = Direction.NULL;
		Position newPosition;
		int stepsAvailable = this.stepsAvailable;
		
		Set<Position> possibleMovements = new HashSet<Position>();
		Deque<Direction> history = new LinkedList<Direction>();
		Map <Position, Deque<Direction>> historyMap = new HashMap <Position, Deque<Direction>>();
		
		StackElement element = new StackElement(this.position, Direction.NULL, 0);
		Stack<StackElement> stack = new Stack<StackElement>();
		stack.push(element);
		
		possibleMovements.add(new Position(this.getPosition().getX(), this.getPosition().getY()));
		history.add(Direction.NULL);
		historyMap.put(new Position(this.getPosition().getX(), this.getPosition().getY()), new LinkedList<Direction>(history));
		
		while(! stack.isEmpty())
		{
			currElement = stack.peek();
			switch(currElement.getLastDirection())
			{
				case NULL:
					newX = -1;
					newY = 0;
					newDirection = Direction.WEST;
					break;
				case WEST:
					newX = 0;
					newY = -1;
					newDirection = Direction.NORTH;
					break;
				case NORTH:
					newX = 1;
					newY = 0;
					newDirection = Direction.EAST;
					break;
				case EAST:
					newX = 0;
					newY = 1;
					newDirection = Direction.SOUTH;
					break;
				case SOUTH:
					newX = 0;
					newY = 0;
					newDirection = null;
					break;
					
			}
			if(stepsAvailable > 0 && null != newDirection)
			{
				currElement = stack.pop();
				currElement.setLastDirection(newDirection);
				stack.push(currElement);
				
				newPosition = new Position(currElement.getPosition().getX() + newX, currElement.getPosition().getY() + newY);
				
				if(isPositionValid(newPosition))
				{
					cost = calculateCostForTerrain(stepsAvailable, newPosition, WorldMap.getInstance().getTerrainTypeAtPosition(newPosition));
					if(stepsAvailable - cost >= 0)
					{
						stepsAvailable -= cost; 
						possibleMovements.add(newPosition);
						
						stack.push(new StackElement(newPosition, Direction.NULL, cost));
						history.addLast(newDirection);
						
						if(historyMap.get(newPosition) == null || historyMap.get(newPosition).size() > history.size()) 
						{
							historyMap.put(newPosition, new LinkedList<Direction>(history));
						}
					}
				}
			}
			else
			{
				currElement = stack.pop();
				stepsAvailable += currElement.getStepsSpent();
				if(!history.isEmpty())
					history.removeLast();
			}
		}
		
		this.possibleMovements = possibleMovements;
		this.historyMap = historyMap;
	}
	
	public void move(Position position)
	{
		this.position = position;
		SceneManager.getInstance().drawSpriteOverTile(position, getSprite());
	}
	
	public Position getPosition() {
		return position;
	}

	public void setPosition(Position position) {
		this.position = position;
	}
	
	public int getStepsAvailable() {
		return stepsAvailable;
	}

	public void setStepsAvailable(int stepsAvailable) {
		this.stepsAvailable = stepsAvailable;
	}

	public Sprite getSprite() {
		return sprite;
	}

	public void setSprite(Sprite sprite) {
		this.sprite = sprite;
	}	
	
	public Set<Position> getPossibleMovements() {
		return this.possibleMovements;
	}
}